package jgrit.control;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;

import jgrit.JGritProperties;
import jgrit.options.BooleanOption;
import jgrit.options.MultiOption;
import jgrit.options.Option;
import jgrit.options.OptionCollection;

import static jgrit.Constants.*;

/**
 * Performs actions described by an {@link OptionCollection} instance
 * using the <tt>grit</tt> command-line tool.
 * 
 * @author Taylor Perkins, Derek Olejnik, David Foster
 */
public class GritService {

	private OptionCollection options;
	
	/**
	 * Initializes a new GritService that can run grit
	 * to execute the operation specified by the specified
	 * set of options.
	 */
	public GritService(OptionCollection options) {
		this.options = options;
	}
	
	/**
	 * Performs the requested operation using grit.
	 * 
	 * @return the status messages printed by grit during execution.
	 * @throws GritException
	 *             if an error occurs while running the grit command-line tool
	 * @throws UnsupportedOperationException
	 *             if the action requested by the specified set of options
	 *             cannot be performed by grit.
	 */
	public String executeCommand() throws GritException {
		String gritExecutable;
		gritExecutable = JGritProperties.getInstance().getProperty(BINARY_LOCATION) + 
			JGritProperties.getInstance().getProperty(GRIT_PATH_PROPERTY);
		
		/*
		 * Separate arguments must be added to the command line individually.
		 * For example, you should add the arguments {"-o", "/home/me/myfile.c"}
		 * separately - it is not valid to try to add a single space-separated
		 * argument "-o /home/me/myfile.c" in lieu of individual arguments.
		 */
		List<String> cmdList = new ArrayList<String>();
		try {
		{
			cmdList.add(gritExecutable);
			
			/* File path */
			if (options.sourceFiles.size() != 1)
				throw new UnsupportedOperationException("Only one output file is supported.");
			cmdList.add(options.sourceFiles.get(0).getAbsolutePath());
			
			/* Output error, warning and status messages (for debugging) */
			cmdList.add("-W3");
			
			/* Graphics Options */
			cmdList.add("-g" + formatGraphicsFormatOption(options.format));
			if (options.format.getValue().equals(GFXFORMAT_BITMAP_NDS)) {
				cmdList.add("-gT!");
			} else if (options.format.getValue().equals(GFXFORMAT_BITMAP_ALPHA_NDS)) {
				cmdList.add("-gT" + options.transColor.getValue());
			}
			cmdList.add("-gB" + options.bitDepth.getValue());
			cmdList.add("-gz" + formatCompressionOption(options.graphicsCompression));

			/* Map Options */
			if (!options.mapLayout.getValue().equals("OFF")) {
				//BUG: since -U8 / -U16 / -U32 don't seem to work
				cmdList.add("-mu" + formatDataTypeOption(options.arrayDataType));
				
				cmdList.add("-mz" + formatMapCompressionOption(options.mapCompression));
				cmdList.add("-mL" + formatMapLayoutOption(options.mapLayout));
				cmdList.add("-mR" + formatMapReductionOptions(options));
				
				cmdList.add("-ma");
				cmdList.add(options.tileOffset.getValue());
			} else {
				cmdList.add("-m!");
			}
			
			/* Meta Tile Options */
			cmdList.add("-Mw" + options.metaTileWidth.getValue());
			cmdList.add("-Mh" + options.metaTileHeight.getValue());
			if (options.metaTilePalReduction.getBooleanValue()) cmdList.add("-MRp");
			
			/* Palette Options */
			if (options.paletteEnabled.getBooleanValue()) {
				cmdList.add("-p" + formatBooleanOption(options.paletteEnabled));
				
				//BUG: since -U8 / -U16 / -U32 don't seem to work
				cmdList.add("-pu" + formatDataTypeOption(options.arrayDataType));
				
				cmdList.add("-ps");
				cmdList.add(options.paletteStart.getValue());
				
				cmdList.add("-pn");
				cmdList.add(options.paletteCount.getValue());
				
				cmdList.add("-pT");
				cmdList.add(options.paletteTrans.getValue());
			}

			/* Area Options */
			if (options.areaIsCustom.getBooleanValue()) {
				cmdList.add("-ah" + options.areaHeight.getValue());
				cmdList.add("-aw" + options.areaWidth.getValue());
				cmdList.add("-al" + options.areaLeft.getValue());
				cmdList.add("-at" + options.areaTop.getValue());
			}
			
			/* File Options */
			cmdList.add("-ft" + formatFileFormatOption(options.fileFormat));
			//BUG: -U... doesn't seem to work right; using alternative
			//cmdList.add("-U" + formatDataTypeOption(options.arrayDataType));
			cmdList.add("-gu" + formatDataTypeOption(options.arrayDataType));
			
			cmdList.add("-fh" + formatBooleanOption(options.includeHeader));
			
			cmdList.add("-o");
			cmdList.add(options.destinationFile.getValue());
			
			cmdList.add("-s");
			cmdList.add(options.symbolBaseName.getValue());
			
			if (options.append.getBooleanValue()) {	
				cmdList.add("-fa");
				
				// Do preprocessing in case the file was created with the pedantic option.
				try {
					PedanticProcessing.preProcessFile(new File(options.destinationFile.getValue()));
					PedanticProcessing.preProcessFile(new File(options.getHeaderFileName()));					
				} catch (IOException e) {
					// Do nothing.  If we get here, the files don't exist, so there is no need to do preprocessing.
				}
			}
			if (options.riffOption.getBooleanValue()) {
				cmdList.add("-fr");
			}
			if (options.externalTileset.hasValue()) {
				cmdList.add("-fx");
				cmdList.add(options.externalTileset.getValue());
			}
		}
		
		
			System.out.println(cmdList);
			
			ProcessBuilder pb = new ProcessBuilder(cmdList);
			// Make sure we get all output sent to stdout and stderr
			pb.redirectErrorStream(true);
			Process grit = pb.start();

			BufferedReader gritOut = new BufferedReader(new InputStreamReader(
					grit.getInputStream()));

			// Process the output of the grit process
			String line;
			StringBuilder sb = new StringBuilder(1000);
			while ((line = gritOut.readLine()) != null) {
				sb.append(line);
				sb.append("\n");
			}
			String statusMessages = sb.toString();
			
			int returnCode = grit.waitFor();
			grit.destroy();
			
			if (returnCode != 0) {
				throw new IOException("The 'grit' process terminated with error " + returnCode + ".");
			}
			
			// Do post-processing of file for C files
			if (options.fileFormat.getValue() == FILEFORMAT_C && options.pedantic.getBooleanValue()) {
				try {
					PedanticProcessing.postProcessFile(new File(options.destinationFile.getValue()));
					PedanticProcessing.postProcessFile(new File(options.getHeaderFileName()));
				} catch (IOException e) {
					IOException e2 = new IOException("I/O error while perform pedantic postprocessing");
					e2.initCause(e);
					throw e2;
				}
			}
			
			
			return statusMessages;
		} catch (Exception e) {
			throw new GritException(e);
		}
	}
	
	private String formatGraphicsFormatOption(MultiOption option) {
		String value = option.getValue();
		if (value.equals(GFXFORMAT_TILE))				return "t";
		if (value.equals(GFXFORMAT_BITMAP_GBA))			return "b";
		if (value.equals(GFXFORMAT_BITMAP_NDS))			return "b";
		if (value.equals(GFXFORMAT_BITMAP_ALPHA_NDS))	return "b";
		throw new IllegalOptionValueError();
	}
	
	private String formatCompressionOption(MultiOption compOption) {
		String value = compOption.getValue();
		if (value.equals(COMPRESSION_OFF))			return "!";
		if (value.equals(COMPRESSION_LZ77))			return "l";
		if (value.equals(COMPRESSION_HUFF))			return "h";
		if (value.equals(COMPRESSION_RLE))			return "r";
		if (value.equals(COMPRESSION_OFFANDHDR))	return "0";
		throw new IllegalOptionValueError();
	}
	
	private String formatFileFormatOption(MultiOption formatOption) {
		String value = formatOption.getValue();
		if (value.equals(FILEFORMAT_C))			return "c";
		if (value.equals(FILEFORMAT_GNUASM))	return "s";
		if (value.equals(FILEFORMAT_BINARY))	return "b";
		if (value.equals(FILEFORMAT_GBFS))		return "g";
		if (value.equals(FILEFORMAT_RIFF))		return "r";
		throw new IllegalOptionValueError();
	}
	
	private String formatDataTypeOption(MultiOption formatOption) {
		String value = formatOption.getValue();
		if (value.equals(DATATYPE_U8))			return "8";
		if (value.equals(DATATYPE_U16))			return "16";
		if (value.equals(DATATYPE_U32))			return "32";
		throw new IllegalOptionValueError();
	}
	
	private String formatMapCompressionOption(MultiOption mapOption) {
		String value = mapOption.getValue();
		if (value.equals(COMPRESSION_OFF))			return "!";
		if (value.equals(COMPRESSION_LZ77))			return "l";
		if (value.equals(COMPRESSION_HUFF))			return "h";
		if (value.equals(COMPRESSION_RLE))			return "r";
		if (value.equals(COMPRESSION_OFFANDHDR))	return "0";
		throw new IllegalOptionValueError();
	}
	
	private String formatMapLayoutOption(MultiOption mapOption) {
		String value = mapOption.getValue();
		if (value.equals(MAPLAYOUT_FLAT))			return "f";
		if (value.equals(MAPLAYOUT_SBB))			return "s";
		if (value.equals(MAPLAYOUT_AFFINE))			return "a";
		throw new IllegalOptionValueError();
	}
			
	private String formatMapReductionOptions(OptionCollection options) {
		boolean flipped = options.mapFlippedReduction.getBooleanValue();
		boolean tile = options.mapTileReduction.getBooleanValue();
		boolean pal = options.mapPalReduction.getBooleanValue();
		if (flipped && tile && pal)			return "tpf";
		else if (tile && flipped)			return "tf";
		else if (tile && pal)				return "tp";
		else if (pal && flipped)			return "pf";
		else if (tile)						return "t";
		else if (pal)						return "p";
		else if (flipped)					return "f";
		else								return "!";
	}
	
	private String formatBooleanOption(BooleanOption option) {
		return option.getBooleanValue() ? "" : "!";
	}
}

/**
 * Thrown when an {@link Option} has an illegal value.
 * 
 * @author David Foster
 */
class IllegalOptionValueError extends AssertionError {
	private static final long serialVersionUID = 1L;

	public IllegalOptionValueError() {
		// nothing
	}
}
